/* Copyright (c) 2015,2016 Lucky Byte, Inc.
 */
package com.lucky_byte.pay.jar;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.Socket;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * 通过 HTTP(S) 与服务器通讯
 */
public class HttpClient
{
	private static final Logger logger = LogManager.getLogger();

	private URL target_url;
	private int timeout;
	private String reqt_charset;
	private String resp_charset;
	private boolean url_encoding;
	private String response;
	private String target_host;
	private int target_port;
	private String local_host;
	private int local_port;

	/**
	 * 建立 HttpClient 对象
	 *
	 * @param target_url 目标服务器地址
	 * @param timeout 超时时间
	 * @param reqt_charset 输入字符编码，例如 'utf-8', 'gb18030' ...
	 */
	public HttpClient(URL target_url, int timeout, String reqt_charset,
			boolean url_encoding) {
		this.target_url = target_url;
		this.timeout = timeout;
		this.reqt_charset = reqt_charset;
		this.url_encoding = url_encoding;
		this.resp_charset = "UTF-8";
	}

	public String getReqtCharset() {
		return this.reqt_charset;
	}

	public String getRespCharset() {
		return this.resp_charset;
	}

	protected void setAddrBySocket(Socket socket) {
		InetAddress r_addr = socket.getInetAddress();
		this.target_host = r_addr.getHostAddress();
		this.target_port = socket.getPort();
		InetAddress l_addr = socket.getLocalAddress();
		this.local_host = l_addr.getHostAddress();
		this.local_port = socket.getLocalPort();
	}

	/**
	 * 获取通讯目的 URL
	 */
	public String getTargetURL() {
		return this.target_url.toString();
	}

	/**
	 * 获取远端主机名
	 */
	public String getTargetHost() {
		return this.target_host;
	}

	/**
	 * 获取远端端口
	 */
	public int getTargetPort() {
		return this.target_port;
	}

	public String getLocalHost() {
		return this.local_host;
	}

	public int getLocalPort() {
		return this.local_port;
	}

	/**
	 * 获取响应数据，应用应该在发送一个请求并成功后调用此方法来获取响应数据，
	 * 否则这个函数的结果是 null 或者无意义的数据。
	 *
	 * @return 上次与服务器通讯的响应数据
	 */
	public String getResponse() {
		return this.response;
	}

	/**
	 * 以 MAP 的方式返回响应数据. 如果不能解析，则返回 null.
	 *
	 * @return MAP 形式的响应数据
	 */
	public Map<String, String> getResponseMap() {
		return this.mapFromKVString(response);
	}

	/**
	 * 发送请求到 target_url，这个函数会和服务器建立新连接，发送请求，读取响应数据，
	 * 最后关闭连接。
	 *
	 * 如果这个函数返回成功(200)，那么返回数据将存放对象内部，后续通过 getResponse()
	 * 可以获取到. 这也意味着如果利用同一个对象连续调用 2 次 doPost()，则前一次的结果
	 * 会被后一次的结果覆盖.
	 *
	 * 事实上不返回 200(OK) 状态，也有可能会从服务器返回数据，但是应用不应该依赖这些数据，
	 * 除非服务器有明确的说明.
	 *
	 * @param params 待发送的数据
	 * @return HTTP 响应码(200 OK)，或者 -1
	 */
	synchronized public int doPost(Map<String, String> params) {
		String message = mapToKVString(params);
		if (message == null) {
			logger.error("处理 HTTP POST 请求参数时出错，发送请求失败.");
			return -1;
		}
		return doPost(message);
	}

	/**
	 * 发送 POST 请求到服务器并接收响应.
	 *
	 * @param message 字符串消息
	 * @return HTTP 响应码(200 OK)，或者 -1
	 */
	synchronized public int doPost(String message) {
		HttpURLConnection connection = getConnection();
		if(connection == null) {
			logger.error("连接到[{}]失败.", target_url.toString());
			return -1;
		}
		try {
			return postMessage(connection, message);
		} catch (Exception e) {
			logger.error("与[{}]通讯错误[{}][{}].", target_url.toString(),
					e.getMessage(), e.getClass().getSimpleName());
			return -1;
		} finally {
			connection.disconnect();
		}
	}

	/**
	 * 打开与目标 URL 的连接，这个时候尚未建立实际的网络连接，直到后续调用 connect().
	 */
	private HttpURLConnection getConnection() {
		try {
			URLConnection connection = target_url.openConnection();
			if (!(connection instanceof HttpURLConnection)) {
				logger.error("[{}]不是 HTTP URL，放弃处理.");
				return null;
			}
			connection.setConnectTimeout(this.timeout * 1000);
			connection.setReadTimeout(this.timeout * 1000);
			connection.setDoInput(true);
			connection.setDoOutput(true);
			connection.setUseCaches(false);
			connection.setAllowUserInteraction(false);
			if (this.url_encoding) {
				connection.setRequestProperty("Content-type",
						"application/x-www-form-urlencoded;charset=" +
								this.reqt_charset);
			} else {
				connection.setRequestProperty("Content-type",
						"text/plain;charset=" + this.reqt_charset);
			}
			if (connection instanceof HttpURLConnection) {
				HttpURLConnection http = (HttpURLConnection) connection;
				http.setInstanceFollowRedirects(true);
			}
			if (connection instanceof HttpsURLConnection) {
				HttpsURLConnection https = (HttpsURLConnection) connection;
				https.setSSLSocketFactory(new NotSafeSocketFactory(this));
				https.setHostnameVerifier(new NoSafeHostnameVerifier());
				return https;
			}
			return (HttpURLConnection) connection;
		} catch (IOException e) {
			logger.error("连接[{}]错误[{}].", target_url.toString(), e);
			return null;
		}
	}

	/**
	 * 发送 HTTP POST 请求，读取服务器响应
	 *
	 * @param connection 链路
	 * @param message 待发送的请求参数
	 * @throws IOException 
	 * @throws InterruptedException 
	 */
	private int postMessage(HttpURLConnection connection, String message)
			throws IOException, InterruptedException {
		// 设置 http 方法
		connection.setRequestMethod("POST");

		// 建立连接，将数据发送到服务端
		int retry = 0;
		while (true) {
			try {
				connection.connect();
				break;
			} catch (Exception e) {
				if (++retry >= 5) {
					logger.error("连接服务器失败[{}][{}].",
							e.getMessage(), e.getClass().getSimpleName());
					throw e;
				}
				logger.warn("第[{}]次连接失败[{}][{}]，等待 1 秒后重试...",
						retry, e.getMessage(), e.getClass().getSimpleName());
				Thread.sleep(1000);
			}
		}
		OutputStreamWriter writer = new OutputStreamWriter(
				connection.getOutputStream(), reqt_charset);
		writer.write(message);
		writer.flush();

		logger.trace("与[{}]建立连接，开始发送请求数据:\n[{}].",
				target_url.toString(), message);

		// 获取响应传输字符集
		String content_type = connection.getContentType();
		if (content_type != null) {
			String[] values = content_type.split(";");
			String charset = "";
			for (String value : values) {
				value = value.trim();
				if (value.toLowerCase().startsWith("charset=")) {
					charset = value.substring("charset=".length());
				}
			}
			if (!charset.trim().isEmpty()) {
				this.resp_charset = charset;
			}
		}
		// 从服务端读取响应代码和响应数据
		BufferedReader reader = null;
		int resp_code = connection.getResponseCode();
		if (resp_code == 200) {
			InputStream recv_s = connection.getInputStream();
			if (recv_s != null) {
				reader = new BufferedReader(
						new InputStreamReader(recv_s, this.resp_charset));
			}
		} else {
			InputStream recv_s = connection.getErrorStream();
			if (recv_s != null) {
				reader = new BufferedReader(
						new InputStreamReader(recv_s, this.resp_charset));
			}
		}
		if (reader != null) {
			StringBuilder builder = new StringBuilder();
			String line;
			while ((line = reader.readLine()) != null) {
				builder.append(line);
			}
			response = builder.toString();
			reader.close();
			logger.trace("[{}]响应HTTP状态码[{}]，响应数据:\n[{}]",
					target_url.toString(), resp_code, response);
		} else {
			logger.warn("服务端[{}]无响应数据.", target_url.toString());
			response = null;
		}
		return resp_code;
	}

	/**
	 * 将 Map 存储的对象，转换为 key=value&key=value 的字符串
	 *
	 * @param params 参数 map
	 * @return 转换后的字符串
	 */
	private String mapToKVString(Map<String, String> params) {
		if (params == null || params.isEmpty()) {
			logger.warn("欲发送的 HTTP 请求参数为空.");
			return null;
		}
		StringBuilder builder = new StringBuilder("");
		for (String key : params.keySet()) {
			try {
				builder.append(key);
				builder.append("=");
				if (this.url_encoding) {
					builder.append(URLEncoder.encode(params.get(key),
							this.reqt_charset));
				} else {
					builder.append(params.get(key));
				}
				builder.append("&");
			} catch (UnsupportedEncodingException e) {
				logger.error("转换请求参数时，不支持编码[{}].", reqt_charset);
				return null;
			}
		}
		return builder.substring(0, builder.length() - 1);
	}

	/**
	 * 将服务器返回的数据，格式为 key=value&key=value 解析成 Map 对象，
	 * 使用 URL 解码规则进行解码.
	 *
	 * @param string 响应数据
	 * @return
	 */
	private Map<String, String> mapFromKVString(String string) {
		if (string == null || string.trim().length() == 0) {
			logger.warn("没有数据可以拆分.");
			return null;
		}
		Map<String, String> result = new HashMap<>();
		String[] array = string.split("&");
		for (String entry : array) {
			String[] kv = entry.split("=", 2);
			result.put(kv[0], kv[1]);
		}
		return result;
	}
}


/**
 * 一个不安全的 SSLSocketFactory，信任所有的服务器.
 */
class NotSafeSocketFactory extends SSLSocketFactory
{
	private SSLContext ssl_context;
	private HttpClient http_client;

	public NotSafeSocketFactory(HttpClient client) {
		try {
			this.ssl_context = SSLContext.getInstance("TLS");
			this.ssl_context.init(null, new TrustManager[] {
					NotSafeX509TrustManager.manger }, null);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		this.http_client = client;
	}

	@Override
	public Socket createSocket(Socket socket, String host,
			int port, boolean auto_close) throws IOException {
		Socket new_sock = this.ssl_context.getSocketFactory().
				createSocket(socket, host, port, auto_close);
		this.http_client.setAddrBySocket(new_sock);
		return new_sock;
	}

	@Override
	public String[] getDefaultCipherSuites() {
		return null;
	}

	@Override
	public String[] getSupportedCipherSuites() {
		return null;
	}

	@Override
	public Socket createSocket(String host, int port)
			throws IOException, UnknownHostException {
		Socket new_sock = this.ssl_context.getSocketFactory()
				.createSocket(host, port);
		this.http_client.setAddrBySocket(new_sock);
		return new_sock;
	}

	@Override
	public Socket createSocket(InetAddress host, int port)
			throws IOException {
		Socket new_sock = this.ssl_context.getSocketFactory()
				.createSocket(host, port);
		this.http_client.setAddrBySocket(new_sock);
		return new_sock;
	}

	@Override
	public Socket createSocket(String host, int port,
			InetAddress local_host, int local_port)
					throws IOException, UnknownHostException {
		Socket new_sock = this.ssl_context.getSocketFactory().
				createSocket(host, port, local_host, local_port);
		this.http_client.setAddrBySocket(new_sock);
		return new_sock;
	}

	@Override
	public Socket createSocket(InetAddress host, int port,
			InetAddress local_host, int local_port) throws IOException {
		Socket result = this.ssl_context.getSocketFactory().
				createSocket(host, port, local_host, local_port);
		this.http_client.setAddrBySocket(result);
		return result;
	}
}

/**
 * 信任所有的服务器
 */
class NotSafeX509TrustManager implements X509TrustManager
{
	static NotSafeX509TrustManager manger = new NotSafeX509TrustManager();

	public NotSafeX509TrustManager() {
	}
	@Override
	public X509Certificate[] getAcceptedIssuers() {
		return null;
	}
	@Override
	public void checkClientTrusted(X509Certificate[] chain, String authType) {
	}
	@Override
	public void checkServerTrusted(X509Certificate[] chain, String authType) {
	}
}

/**
 * 信任所有的主机地址.
 */
class NoSafeHostnameVerifier implements HostnameVerifier
{
	@Override
	public boolean verify(String hostname, SSLSession session) {
		return true;
	}
}
